use syn::{Ident, PatIdent, PatType};
use syn::{ItemFn, Pat, Type};
use crate::extract::RouteAttr;

#[derive(Default)]
pub struct ParamInfo {
    pub all_params_names: Vec<Ident>,

    pub path_params: Vec<(String, Type)>,
    pub query_params: Vec<(String, Type)>,
    pub body_param: Option<(String, Type)>,
    pub state_param: Option<(String, Type)>,
}

impl ParamInfo {
    pub fn extracted(func: &ItemFn, attr: &RouteAttr) -> Result<Self, proc_macro2::TokenStream> {
        let mut param_info = Self::default();
    
        for param in func.sig.inputs.iter() {
            match param {
                syn::FnArg::Typed(pat_type) => {
                    match &*pat_type.pat {
                        // t: T
                        Pat::Ident(ident) => {
                            param_info.classify_and_add_param(attr, ident, pat_type)?;
                        }
                        // T(t): T
                        Pat::TupleStruct(pat_ts) => {
                            let elems = &pat_ts.elems;
                        
                            if elems.len() == 1 {
                                let inner_pat = &elems[0];
                        
                                if let Pat::Ident(inner_ident) = inner_pat {
                                    param_info.classify_and_add_param(attr, inner_ident, pat_type)?;
                                } else {
                                    return Err(syn::Error::new_spanned(
                                        inner_pat,
                                        "Unsupported inner pattern inside `Json(...)`. Only `Json(name): Json<T>` is supported."
                                    ).to_compile_error());
                                }
                            } else {
                                return Err(syn::Error::new_spanned(
                                    elems,
                                    "Only single-element tuple structs like `Json(name): Json<T>` are supported."
                                ).to_compile_error());
                            }
                        }
                        pat => {
                            return Err(syn::Error::new_spanned(
                                pat,
                                "Unsupported parameter pattern. Only ident patterns are supported, like `param: Type`."
                            )
                            .to_compile_error());
                        }
                    }
                }
                // &self ...
                syn::FnArg::Receiver(self_arg) => {
                    return Err(syn::Error::new_spanned(
                        self_arg,
                        "`self` is not supported in route handler functions."
                    )
                    .to_compile_error());
                }
            }
        }

        param_info.check(attr)?;
    
        Ok(param_info)
    }

    fn classify_and_add_param(&mut self, attr: &RouteAttr, ident: &PatIdent, ty: &PatType) -> Result<(), proc_macro2::TokenStream> {
        let name = ident.ident.to_string();
        let ty = (*ty.ty).clone();
    
        self.all_params_names.push(ident.ident.clone());
        
        if attr.is_body_param(&ident.ident) {
            self.body_param = Some((name, ty));
        } else if attr.is_state_param(&ident.ident) {
            self.state_param = Some((name, ty));
        } else if attr.is_query_param(&ident.ident) {
            self.query_params.push((name, ty));
        } else if attr.is_path_param(&ident.ident) {
            self.path_params.push((name, ty));
        } else {
            return Err(syn::Error::new_spanned(
                ident,
                format!("Unexpected parameter `{}`, not specified as path, query, body or state", ident.ident),
            )
            .to_compile_error());
        }
    
        Ok(())
    }     

    fn check(&self, attr: &RouteAttr) -> Result<(), proc_macro2::TokenStream> {
        // If attr spec the body param, it should exit in handle
        if let Some(body_param) = &attr.body_param {
            if self.body_param.is_none() {
                return Err(syn::Error::new_spanned(
                    body_param, 
                    format!("Body parameters `{}` specified but not used", body_param.to_string())
                ).to_compile_error());
            }
        }

        // If state spec the body param, it should exit in handle
        if let Some(state_param) = &attr.state_param {
            if self.state_param.is_none() {
                return Err(syn::Error::new_spanned(
                    state_param, 
                    format!("State parameters `{}` specified but not used", state_param.to_string())
                ).to_compile_error());
            }
        }

        // Check query param
        for expected_param in attr.query_param.iter() {
            if self.query_params.iter().all(|(name, _)| *name != expected_param.to_string()) {
                return Err(syn::Error::new_spanned(
                    expected_param, 
                    format!("Expected query parameter `<{}>` is not found in function parameters", expected_param)
                ).to_compile_error());
            }
        }

        // Check path param
        for expected_param in attr.path_param.iter() {
            if self.path_params.iter().all(|(name, _)| *name != expected_param.to_string()) {
                return Err(syn::Error::new_spanned(
                    expected_param, 
                    format!("Expected path parameter `<{}>` is not found in function parameters", expected_param)
                ).to_compile_error());
            }
        }

        assert_eq!(attr.path_param.len(), self.path_params.len(), "Path param panic");
        assert_eq!(attr.query_param.len(), self.query_params.len(), "Query param panic");
        assert_eq!(attr.body_param.is_some(), self.body_param.is_some(), "Body param panic");
        assert_eq!(attr.state_param.is_some(), self.state_param.is_some(), "State param panic");

        Ok(())
    }
}

